/**
 * Copyright (c) 2005-2012 springside.org.cn
 */
package com.anuo.app.common.security;


import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;


/*
* 安全相关类
* */
public class SecurityUtil {

    private static SecurityUtil instance;

    private static SecurityUtil getInstance() {
        if (instance == null) {
            instance = new SecurityUtil();
        }
        return instance;
    }

    //region aes 加密解密
    //使用的时候 将下面的 key 和 initVector 改成一个和应用程序相关的指定值就可以了
    //参考: https://stackoverflow.com/questions/15554296/simple-java-aes-encrypt-decrypt-example
    private static final String key = "ydlydlydlydlydly"; // 128 bit key 使用的时候改变里面的字母即可, 位数和现在保持一致
    private static final String initVector = "0102030405060708"; // 16 bytes IV 使用的时候改变里面的数字即可, 位数和现在保持一致

    public static String aesEncrypt(String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());
//            System.out.println("encrypted string: "
//                    + Base64.encodeBase64String(encrypted));

            return Base64.encodeBase64String(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }


    public static String aesDecrypt(String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }
    //endregion aes 加密解密

    //region PBKDF2 用户密码加密
    //参考: https://stackoverflow.com/questions/2860943/how-can-i-hash-a-password-in-java
    /**
     * Each token produced by this class uses this identifier as a prefix.
     */
    private static final String ID = "$31$";

    /**
     * The minimum recommended cost, used by default
     */
    private static final int DEFAULT_COST = 16;

    private static final String ALGORITHM = "PBKDF2WithHmacSHA1";

    private static final int SIZE = 128;

    private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");

    private final SecureRandom random;

    private final int cost;

    private SecurityUtil() {
        this(DEFAULT_COST);
    }

    /**
     * Create a password manager with a specified cost
     *
     * @param cost the exponential computational cost of hashing a password, 0 to 30
     */
    private SecurityUtil(int cost) {
        iterations(cost); /* Validate cost */
        this.cost = cost;
        this.random = new SecureRandom();
    }

    private static int iterations(int cost) {
        if ((cost & ~0x1E) != 0)
            throw new IllegalArgumentException("cost: " + cost);
        return 1 << cost;
    }

    /**
     * Hash a password for storage.
     *
     * @return a secure authentication token to be stored for later authentication
     */
    private String hash(char[] password) {
        byte[] salt = new byte[SIZE / 8];
        random.nextBytes(salt);
        byte[] dk = pbkdf2(password, salt, 1 << cost);
        byte[] hash = new byte[salt.length + dk.length];
        System.arraycopy(salt, 0, hash, 0, salt.length);
        System.arraycopy(dk, 0, hash, salt.length, dk.length);
        java.util.Base64.Encoder enc = java.util.Base64.getUrlEncoder().withoutPadding();
        return ID + cost + '$' + enc.encodeToString(hash);
    }

    /**
     * Authenticate with a password and a stored password token.
     *
     * @return true if the password and token match
     */
    private boolean authenticate(char[] password, String token) {
        Matcher m = layout.matcher(token);
        if (!m.matches())
            throw new IllegalArgumentException("Invalid token format");
        int iterations = iterations(Integer.parseInt(m.group(1)));
        byte[] hash = java.util.Base64.getUrlDecoder().decode(m.group(2));
        byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
        byte[] check = pbkdf2(password, salt, iterations);
        int zero = 0;
        for (int idx = 0; idx < check.length; ++idx)
            zero |= hash[salt.length + idx] ^ check[idx];
        return zero == 0;
    }

    private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) {
        KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
        try {
            SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
            return f.generateSecret(spec).getEncoded();
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
        } catch (InvalidKeySpecException ex) {
            throw new IllegalStateException("Invalid SecretKeyFactory", ex);
        }
    }

    /**
     * hash加密用户密码(用PBKDF2)
     * @param password
     * @return
     */
    public static String hashPassword(String password) {
        return SecurityUtil.getInstance().hash(password.toCharArray());
    }

    /**
     * 鉴别密码是否正确(用PBKDF2)
     * @param password
     * @param hashedPassword
     * @return
     */
    public static boolean authenticatePassword(String password, String hashedPassword) {
        return SecurityUtil.getInstance().authenticate(password.toCharArray(), hashedPassword);

    }


    //endregion

    public static void main(String[] args) {
        //region aes 加密调用
        String msg = aesEncrypt("hello");
        System.out.println("encrypt : "+msg);
        String msgDecrypt = aesDecrypt(msg);
        System.out.println("Decrypt : "+msgDecrypt);
        //endregion
//        SecurityUtil security = new SecurityUtil();
//        String token = security.hash("qaz.00");
//        boolean result = security.authenticate("qaz.00", token);

    }

}
